![]() ![]() ![]() ![]() ![]() |
Charlie Calvert's C++ Builder Unleashed
- 3 -
|
StringOne | StringTwo | Result |
England | England | Returns zero |
England | France | Return a negative number |
France | England | Returns a positive number |
The Pos method is used to find the offset of a substring within a larger string. Here is a simple example from the UsingAnsiString program of how to use the Pos function:
void __fastcall TForm1::SimpleString1Click(TObject *Sender) { AnsiString S = "Sammy"; Memo1->Text = S; int i = S.Pos("mm"); S.Delete(i + 1, 2); Memo1->Lines->Add(S);
}
This code creates an AnsiString initialized to the name "Sammy". It then searches through the string for the place where the substring "mm" occurs, and returns that index so that it can be stored in the variable i. I then use the index as a guide when deleting the last two characters from the string, thereby transforming the word "Sammy" to the string "Sam".
Here is a similar case, except that this time code searches for a tab rather than an ordinary substring:
void __fastcall TForm1::Pos1Click(TObject *Sender) { AnsiString S("Sammy \t Mike"); Memo1->Text = S; AnsiString Temp = Format("The tab is in the %dth position", OPENARRAY(TVarRec, (S.Pos(`\t')))); Memo1->Lines->Add(Temp); }
The code in this example first initializes the AnsiString S to a string that contains a tab, and then searches through the string and reports the offset of the tab: "The tab is in the 7th position." The Format function shown here works in the same fashion as sprintf. In fact, the following code would have the same result as the Pos1Click method shown previously:
AnsiString S("Sammy \x009 Mike"); Memo1->Text = S; char Temp[75]; sprintf(Temp, "The tab is in the %dth position", S.Pos(`\t')); Memo1->Lines->Add(Temp);
C++ uses a series of escape sequences to represent special characters such as
tabs, backspaces, and so on. If you are not familiar with C++, you might find the
following table of escape sequences useful:
Human readable name | Escape sequence | Hex representation |
Bell | \a | \x007 |
Backspace | \b | \x008 |
Tab | \t | \x009 |
Newline | \n | \x00A |
Form Feed | \f | \x00C |
Carriage return | \r | \x00D |
Double quote | \" | \x022 |
Single quote | \' | \x027 |
Backslash | \\ | \x05C |
Escape sequences are usually placed in single quotes, though in the previous case it would not matter whether I used single or double quotes in the Pos statement.
Using hex values has the exact same effect as using escape sequences. For instance, the following code creates identical output to the Pos1Click method shown previously:
AnsiString S("Sammy \x009 Mike"); Memo1->Text = S; AnsiString Temp = Format("The tab is in the %dth position", OPENARRAY(TVarRec, (S.Pos(`\x009')))); Memo1->Lines->Add(Temp);
Though some programmers may find it simpler and more intuitive to use the Hex values shown in the last column of the table, the arcane-looking escape sequences cling on in C++ code because they are portable to platforms other than Windows and DOS.
In this section you will see how to declare an array of AnsiStrings. The text includes examples of how to access them as standard null-terminated strings, and how to use them in moderately complex circumstances.
The following ChuangTzu program uses some Chinese quotes with a millennium or two of QA under their belt to illustrate the favorite theme of this book:
Easy is right. Begin right And you are easy. Continue easy and you are right. The right way to go easy Is to forget the right way And forget that the going is easy.
This program declares this string as an AnsiString, and then passes it to a procedure that can break it down into a series of words. These words are passed back one at a time and added into an array. The array is then displayed in a series of one word lines in a memo control. The program appears in Listings 3.1 and 3.2.
//-------------------------------------------------------------------------- #ifndef MainH #define MainH //-------------------------------------------------------------------------- #include <Classes.hpp> #include <Controls.hpp> #include <StdCtrls.hpp> #include <Forms.hpp> //-------------------------------------------------------------------------- class TChuangTzu { private: int FCount; AnsiString FWords[40]; AnsiString GetWords(int Index); void SetWords(int Index, AnsiString S); public: TChuangTzu(){ FCount = 0; } __property AnsiString Words[int Index] = {read=GetWords, write=SetWords}; __property int Count={read=FCount, write=FCount}; }; class TForm1 : public TForm { published: TButton *Button1; TMemo *Memo1; void __fastcall Button1Click( TObject *Sender); private: public: virtual __fastcall TForm1(TComponent* Owner); }; //-------------------------------------------------------------------------- extern TForm1 *Form1; //--------------------------------------------------------------------------
#endif
#include <vcl.h> #pragma hdrstop #include "Main.h" #include "codebox.h" #pragma resource "*.dfm" TForm1 *Form1; /////////////////////////////////////// // ChuangTzu ////////////////////////// /////////////////////////////////////// AnsiString TChuangTzu::GetWords(int i) { return FWords[i]; } void TChuangTzu::SetWords(int i,const AnsiString s) { FCount++; FWords[i]=s; } AnsiString StripWords(AnsiString &S, char Token) { AnsiString TokenStr(Token); AnsiString Temp1; AnsiString Temp2 = S + ` `; Temp1 = strtok(S.c_str(), TokenStr.c_str()); Temp2 = strrev(Temp2.c_str()); Temp2.SetLength(Temp2.Length() - (Temp1.Length() + 1)); S = strrev(Temp2.c_str()); return Temp1; } /////////////////////////////////////// // Form1 ////////////////////////////// /////////////////////////////////////// __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } void __fastcall TForm1::Button1Click( TObject *Sender) { TChuangTzu C; AnsiString S1; int i = 0; AnsiString S = "Easy is right. Begin right "; "And you are easy. " "Continue easy and you are right. " "The right way to go easy " "Is to forget the right way " "And forget that the going is easy."; do { S1 = StripWords(S, ` `); C.Words[i] = S1; i++; } while (S1 != ""); for (i = 0; i < C.Count; i++) { Memo1->Lines->Add(C.Words[i]); }
}
The core of this program is the ChuangTzu class definition:
class TChuangTzu { private: int FCount; AnsiString FWords[40]; AnsiString GetWords(int Index); void SetWords(int Index, AnsiString S); public: TChuangTzu(){ FCount = 0; } __property AnsiString Words[int Index] = {read=GetWords, write=SetWords}; __property int Count={read=FCount, write=FCount};
};
At the heart of the object is the declaration for an array of 40 AnsiStrings:
AnsiString FWords[40];
You can access the second member of this array with code that looks like this:
FWords[1] = "Let some sad trumpeter stand on the empty streets at dawn." AnsiString S = FWords[1];
The ChuangTzu object provides an array property that gives you access to this private data. There is, of course, no reason why you can't declare an array of AnsiStrings on their own without a supporting object. However, I thought it might be helpful to see how you can use an array in your program without breaking the rule about keeping the raw data of your objects private.
Here is the declaration for the array property:
__property AnsiString Words[int Index] = {read=GetWords, write=SetWords};
As you can see, it returns an AnsiString and can be indexed with an integer value. (There is nothing to keep you from declaring array properties that are indexed with AnsiStrings!)
Here is the get method for the array:
AnsiString TChuangTzu::GetWords(int i) { return FWords[i]; }
Notice that it takes a single integer as a parameter and returns an AnsiString.
Here is the set method for the array:
void TChuangTzu::SetWords(int i,const AnsiString s) { FCount++; FWords[i]=s; }
This function takes both an integer and a string as parameters. In order to call this method, you write
ChuangTzu.Words[i] = "peace proclaims olives of endless age."
Here you can see the integer that is passed to SetWords, as well as a quote from Shakespeare's 107 sonnet that is passed in the second parameter. The FCount variable is used to track the number of entries in the array. Note that properties provide an improvement over get and set methods by letting you use simple array syntax.
Here is an example of how to declare a large AnsiString that extends over several lines:
AnsiString S = "Easy is right. Begin right "; "And you are easy. " "Continue easy and you are right. " "The right way to go easy " "Is to forget the right way " "And forget that the going is easy.";
The StripWords function is used to peel off words from this quote one at a time, starting at the front:
AnsiString StripWords(AnsiString &S, char Token) { AnsiString TokenStr(Token); AnsiString Temp1; AnsiString Temp2 = S + ` `; Temp1 = strtok(S.c_str(), TokenStr.c_str()); Temp2 = strrev(Temp2.c_str()); Temp2.SetLength(Temp2.Length() - (Temp1.Length() + 1)); S = strrev(Temp2.c_str()); return Temp1; }
There are two interesting things about this function. The first shows how to concatenate strings with the plus operator, and the second shows how to use standard C string functions on an AnsiString.
Notice the bit of syntax that adds a space onto the end of the string passed to the function:
AnsiString Temp2 = S + ` `;
You can always use the plus operator to append or prefix information to an AnsiString. This eliminates any need for you to use the strcat function, though strcat is a likely candidate for the routine inside the AnsiString implementation that actually performs the concatenation.
NOTE: The reason I append a space onto the end of this string is to ensure that the AnsiString object that underlies this C string makes a unique copy of it. If I simply assign the two strings without changing them, the call to strrev or strtok will affect both strings!
Note also that AnsiStrings support the += operator. Therefore you don't have to write S = S + "Sam", but can write S += "Sam".
I do, however, use standard C functions to tokenize and reverse the strings:
Temp1 = strtok(S.c_str(), TokenStr.c_str()); Temp2 = strrev(Temp2.c_str()); Temp2.SetLength(Temp2.Length() - (Temp1.Length() + 1));
What the function does here is not important. Just notice that I can get at the underlying null-terminated string with the c_str() method of the AnsiString class. Once I have access to the underlying string, I can pass it to any of the standard C functions such as strrev, strcat, strtok, and so on.
To get the length of a string, call the Length method of the AnsiString class:
int I = MyString.Length();
I have gone on at some length about the AnsiString class because it is used extensively throughout this book and throughout most BCB programs. The key fact to remember is that AnsiStrings are compatible with the VCL. For instance, you can assign an AnsiString directly to a VCL control:
AnsiString S("Doubting the filching age will steal his treasure"); Edit1->Text = S;
You can also assign a regular C string to a VCL control, but that is because the VCL string is represented on the C++ side as an AnsiString, which can have its constructor called by a null-terminated string:
char S[] = {"Till either gorge be stuffed or prey be gone"}; Memo1->Text = S;
NOTE: This time the quote is from Shakespeare's Venus and Adonis, with this seemingly gruesome line appearing in the following context:
Till either gorge be stuffed or prey be gone
Even so she kiss'd his brow, his cheek, his chin,
And where she ends she doth anew begin.
The bard's point being, I suppose, that her appetites were large.
In fact, you can take advantage of these constructors and assign an integer directly to a VCL control:
Memo1->Text = 2;
This is considerably more freedom than the strongly typed Object Pascal language would ever give you liberty to pursue.
This chapter is largely about the places where C++ and the VCL are woven together. Most of the code that is used to accomplish this end is found in SYSDEFS.H. SYSDEFS.H is the place where the developers put most of the core system code needed to allow BCB to use the VCL native types. In particular, if there was some element of Object Pascal not supported by C++ that played a role in the VCL, solutions to that problem were mapped out in this file. The only major exception to this rule is DSTRINGS.H, where the AnsiString class is declared.
NOTE: SYSDEFS.H is to some degree a testament to the power of the C++ language. It features classes and templates that emulate features of Object Pascal that Delphi relies on the compiler to implement. The advantage of having the compiler implement these features is that it is possible to pick and choose exactly what kind of syntax you want to use. Thus the elegance and simplicity of the Object Pascal language. On the other hand, the extraordinary richness and flexibility of the C++ object model is highlighted beautifully by the work done in SYSDEFS.H.
The following classes appear in SYSDEFS.H:
class __declspec(delphireturn) Currency : public CurrencyBase class __declspec(delphireturn) TDateTime : public TDateTimeBase class __declspec(delphiclass) TObject class TMetaClass class __declspec(delphireturn) Set class TVarArray class TVarData class TVarRec template<class T> class OpenArray class __declspec(delphireturn) Variant: public TVarData class AutoCmd class NamedParm class Procedure: public AutoCmd class Function: public AutoCmd class PropertySet: public AutoCmd class PropertyGet: public AutoCmd typedef class TMemoryManager *PMemoryManager; class THeapStatus;
Some of these classes, such as TObject and TMetaClass, have already been discussed at length. Other classes such as Procedure, Function, PropertyGet, and PropertySet are part of the guts of the BCB-VCL interface, and are not worth discussing in this context. But a few of these classes, notably Set, Currency, TDataTime, Variant, and TMemoryManager play an important role in standard BCB programming, and will therefore be covered in the next few pages. I will also take a look at TVarRec and OpenArray, because they play an important supporting role in the object model.
In the next three sections, called TDateTime, TCurrency, and OpenArrays, I explore some of the ways you can format text before presenting it to the user. You should read through all three sections to get a feeling for some of the options supplied by BCB.
Many tools for formatting text have been contributed by third-party VCL vendors. There are many sites on the Web that provide links to third-party toolmakers. To get started, visit the Link section of my Web site: users.aol.com/charliecal, or go to www.borland.com.
The VCL comes equipped with a number of functions for working with the current date or time, as shown by this excerpt from the online help:
Date | Returns the current date |
DateTimeToStr | Converts a value from time format to a string |
DateTimeToString | Converts a value from time format to a string |
DateToStr | Converts a value from date format to a string |
DayOfWeek | Returns the current day of the week |
DecodeDate | Decodes the specified date |
DecodeTime | Decodes the specifies time |
EncodeDate | Returns values specified in date format |
EncodeTime | Returns values specified in time format |
FormatDateTime | Formats a date and time using the specified format |
Now | Returns the current date and time |
StrToDate | Coverts a string to a date format |
StrToDateTime | Converts a string to a date/time format |
StrToTime | Converts a string to a time format |
Time | Returns the current time |
TimeToStr | Converts a time format to a string |
There is also a TDateTimeField that is used in database programming.
Many of these functions work with an object type called TDateTime that is found in the SYSDEFS.H unit. Most of the rest of this section is dedicated to exploring the uses of the TDateTime type. This type exists in order to provide compatibility with the VCL routines listed at the beginning of this section.
The following method shows two different ways of outputting the current date:
void __fastcall TForm1::CurrentDate1Click(TObject *Sender) { TDateTime Date1, Date2 = Now(); AnsiString S; DateTimeToString(S, "dddd, mmmm dd, yyyy", Date1.CurrentDate()); S += "\r" + Date2.FormatString("dddd, mmmm dd, yyyy"); ShowMessage(S); }
The code shown in the CurrentDate1Click method outputs the following text inside a RichEdit control:
Tuesday, January 07, 1997 Tuesday, January 07, 1997
NOTE: The TRichEdit control from the Win95 page of the Component Palette provides supports for the RTF format. There is no set limit on the amount of text you can display in a TRichEdit control, and it supports the use of multiple fonts and colors inside a single control. There are also routines for saving the loading and saving the contents of the control, and for printing.
I initialize the first of the two TDateTime objects with the default constructor, which automatically sets the date to Saturday, December 30, 1899. Then set asks this object to retrieve the current date (not the default date), which it passes to me in the form of a TDateTime object:
static TDateTime __fastcall CurrentDate();
A raw TDateTime object is obviously not something I can show to the user, so I use the VCL DateTimeToString routine to convert it into a string:
DateTimeToString(S, "dddd, mmmm dd, yyyy", Date1.CurrentDate());
This routine takes a string in the first parameter, a format string in the second parameter, and a TDateTime record in the third parameter.
NOTE: If you look in SYSDEFS.H, you will see that TDateTime is declared delphireturn. That is because this object needs to conform to the expectations of VCL routines such as DateTimeToString. In other words, this is a classic example of what use the BCB team made of delphireturn.
The second TDateTime object shown in this example is set equal to the current date during its construction:
TDateTime Date2(Now());
Notice that I pass the VCL routine called Now to the TDateTime constructor. Now returns a variable of type TDateTime that is initialized to the current date and time. You can then use the FormatString method of the TDateTime object to retrieve the current string:
S += "\n" + Date2.FormatString("dddd, mmmm dd, yyyy");
The FormatString method takes the same type of format string passed to DateTimeToString.
Format strings are documented in the online help, but the basic idea behind them is that if you pass in two letters, you get a number back; pass in three letters, and you get an abbreviation back; and pass in four letters, and you get a whole word back:
dd => 01 ddd => Sun dddd => Sunday
The following method displays the current time in a specified format:
void __fastcall TForm1::CurrentTime1Click(TObject *Sender) { TDateTime Date(Now()); RichEdit1->Text = Date.FormatString("h:nn:ss am/PM"); }
The result will look something like this:
12:27:56 PM
In this case, the PM is in large letters because I wrote it that way in the format specifier. Had I written am/pm, the resulting string would have looked like this: 12:27:56 pm. The rest of the format specifiers works like this:
h => No leading zero hh => Prefix a leading zero
In short, if you pass in one letter, you get back a string with no leading zero; if you pass in two, a leading zero will be added automatically. Notice that you pass in an n to specify the format for minutes. This is because m has already been used for months.
Here is a table listing the various specifiers you can use:
Format specifier | Item |
s | Seconds |
m | Minutes |
h | Hour |
d | Day |
m | Month |
y | Year |
am/pm | Format for time |
c | Uses ShortDateFormat and LongTimeFormat |
t | Uses ShortTimeFormat |
tt | Uses LongTimeFormat |
ddddd | Display date using ShortDateFormat |
dddddd | Display date using LongDateFormat |
The various date and time formats shown in the last items will be explained in just one moment.
The DateTimeString., DateString, and TimeString methods of the TDateTime object do not require that you provide format specifiers. Here, for instance, is an example of how to use the DateTimeString method:
void __fastcall TForm1::DateandTime1Click(TObject *Sender) { TDateTime Date(Now()); RichEdit1->Lines->Add(Date.DateTimeString());
}
On my system, the code shown here produces the following result:
1/7/97 12:31:24 PM
The format used in this case is global to the system. You can change these settings by working with a series of global variables found in SYSUTILS.HPP:
extern System::AnsiString CurrencyString; extern unsigned char CurrencyFormat; extern unsigned char NegCurrFormat; extern char ThousandSeparator; extern char DecimalSeparator; extern unsigned char CurrencyDecimals; extern char DateSeparator; extern System::AnsiString ShortDateFormat; extern System::AnsiString LongDateFormat; extern char TimeSeparator; extern System::AnsiString TimeAMString; extern System::AnsiString TimePMString; extern System::AnsiString ShortTimeFormat; extern System::AnsiString LongTimeFormat; extern System::AnsiString ShortMonthNames[12]; extern System::AnsiString LongMonthNames[12]; extern System::AnsiString ShortDayNames[7];
extern System::AnsiString LongDayNames[7];
On my system, these values are preset as follows:
CurrencyString: $ CurrencyFormat: 0 NegCurrFormat: 0 ThousandSeparator: , DecimalSeparator: . CurrencyDecimals: 2 DateSeparator: / ShortDateFormat: M/d/yy LongDateFormat: dddd, MMMM dd, yyyy TimeSeparator: : TimeAMString: AM TimePMString: PM ShortTimeFormat: h:mm AMPM LongTimeFormat: h:mm:ss AMPM ShortMonthNames: Jan ShortMonthNames: Feb ShortMonthNames: Mar ShortMonthNames: Apr ShortMonthNames: May ShortMonthNames: Jun ShortMonthNames: Jul ShortMonthNames: Aug ShortMonthNames: Sep ShortMonthNames: Oct ShortMonthNames: Nov ShortMonthNames: Dec LongMonthNames: January LongMonthNames: February LongMonthNames: March LongMonthNames: April LongMonthNames: May LongMonthNames: June LongMonthNames: July LongMonthNames: August LongMonthNames: September LongMonthNames: October LongMonthNames: November LongMonthNames: December ShortDayNames: Sun ShortDayNames: Mon ShortDayNames: Tue ShortDayNames: Wed ShortDayNames: Thu ShortDayNames: Fri ShortDayNames: Sat LongDayNames: Sunday LongDayNames: Monday LongDayNames: Tuesday LongDayNames: Wednesday LongDayNames: Thursday LongDayNames: Friday LongDayNames: Saturday
For instance, the following code changes the nature of the current ShortDateFormatString:
void __fastcall TForm1::SetShortDateFormattoMMMMDDDDYYYY1Click(TObject *Sender) { RichEdit1->Text = Now(); ShortDateFormat = "MMMM/DDDD/YYYY"; RichEdit1->Lines->Add(Now());
}
The text displayed by calling this function is as follows:
1/7/97 1:30:21 PM January/Tuesday/1997 1:30:21 PM
The first block of text shows the default behavior, and the second block shows what happened after I made a few subtle changes to the system.
The system initializes the date and time strings to the choices you make in Windows. You can see the current Windows settings by calling GetLocaleInfo. Changes you make are written to the system registry if you send a WM_INICHANGE message to your own nonconsole mode application.
The following lines of code show how you can compare two times using the greater than operator:
void __fastcall TForm1::SetTimeOne1Click(TObject *Sender) { FTimeOne = Now(); RichEdit1->Text = FTimeOne.TimeString(); } void __fastcall TForm1::SetTimeTwo1Click(TObject *Sender) { FTimeTwo = Now(); RichEdit1->Lines->Add(FTimeTwo.TimeString()); } void __fastcall TForm1::CompareOnetoTwo1Click(TObject *Sender) { AnsiString S; if (FTimeOne > FTimeTwo) S = FTimeOne.TimeString() + " > " + FTimeTwo.TimeString(); else S = FTimeTwo.TimeString() + " > " + FTimeOne.TimeString(); RichEdit1->Lines->Add(S);
}
The first method shown here sets a global object called TimeOne to the current time. The second method sets a second global object to the current time. After calling the two functions at a two-second interval, you can end up with the objects initialized to two different times:
2:10:29 PM 2:10:31 PM
You can then use the third function to compare them using the > operator:
2:10:31 PM > 2:10:29 PM
You can also use the ++ operator on the current time to increment the day by one:
void __fastcall TForm1::CompareTwotoOne1Click(TObject *Sender) { int i; RichEdit1->Lines->Add(FTimeOne.DateTimeString()); FTimeOne++; RichEdit1->Lines->Add(FTimeOne.DateTimeString()); }
Here is the output in the RichEdit control after a run off all four functions:
2:13:03 PM 2:13:05 PM 2:13:05 PM > 2:13:03 PM // First call to compare 1/7/97 2:13:03 PM 1/8/97 2:13:03 PM 2:13:03 PM > 2:13:05 PM // Second call to compare
As you can see, calling the ++ operator incremented the day by one. After incrementing the values, a second call to the compare function showed that FTimeOne is now larger than FTimeTwo. Even though it is still two seconds smaller than date two, it is now a day later, and hence larger.
This should give you some sense of what you can do with the TDateTime type. This has not been an exhaustive investigation of what can be done, but if you now open up SysDefs.h, you should have no trouble following the drift of the declaration for the other methods in the TDateTime object. The code samples shown in this section are from a program found on disk called, in a flight of poetic fancy, DateTime1.
I will now spend a few minutes looking at TCurrency. After the rather lengthy investigation of TDateTime that you just saw, there should not be much need to examine this object in-depth, because it follows the same general patterns of logic you saw in the previous section.
When following this discussion, remember that the primary reason the type exists is for compatibility with the VCL. In particular, the TCurrency type, like the TDateTime type, is used in database programming. For instance, there is a TCurrencyField and a TDateTimeField available for database programmers. The principles behind the field types are examined in detail throughout the lengthy database section of this book, Chapters 8 through 18, and particularly in Chapter 11, "Working with Field Objects."
The Currency type is a wrapper around the built in __int64 type. The compiler probably will not support assigning large numbers directly to a variable of type Currency:
Currency C = 5000000000000; // This probably won't work
You can, however, pass in a string that holds a large number. Consider the following method:
Currency C = void __fastcall TForm1::Button1Click(TObject *Sender) { Currency C("500000000000000"); AnsiString S = C; Edit1->Text = S; Edit2->Text = Format("%m", OPENARRAY(TVarRec, (C)));
}
This method initializes a variable of type currency to a large number with a string. Most of the time large numbers will be entered by the user in an edit control, so this is a reasonable way to proceed.
Once you have an instance of a Currency type, you can display it directly to the user or assign it to a TCurrencyField from a database table. Notice that you can assign a string directly to a Currency field, and vice versa. When translating a currency type to a string, the result does not produce a formatted result string. Instead, you see a plain number:
500000000000000
If you want to display the string with proper formatting, use the Format function, as shown in the Button1Click method:
$500,000,000,000,000.00
I will describe the Format function, and OpenArrays, in the next section of this chapter, called "Working with OpenArrays." Note that some of the database controls will automatically display a currency type with formatting, depending in part on their attributes and the contents of the Data Dictionary.
The currency type will handle numbers in the following range:
-922337203685477.5808..922337203685477.5807
That is, it can handle between 19 and 20 digits.
There are many operators in the Currency class for performing mathematical operations. For instance, the following syntax is valid:
Currency C1 = 25000, C2 = 5000, C3; C1 += C2; C1 = C2 / 9; C3 = (C1 % C2); C3 = (C1 + C2) / C1 + (C1 % C2);
It's interesting to note that these operations are performed on objects, and not on simple types. This is an excellent example of the power of C++ and the amazing things you can do with operator overloading. If you are not used to C++, you should spend a few moments contemplating the fact that C1, C2, and C3 are objects, with methods and functions, and not just simple built-in types like integers or floats. You should open up SysDefs.h to see a list of all the supported syntax used by the Currency object.
The native VCL TMaskEdit control deserves at least a few sentences of comment during this discussion of formatting strings. This component is a descendant of TEdit that enables you to control the kinds of characters that are entered into the control.
The key to the TMaskEdit component is the EditMask property, which enables you to enter a string that uses several special characters to define what the user can legally enter into the edit area.
There is a property editor associated with the EditMask property. In this editor, you can find a number of default masks, as shown in Figure 3.1. However, on many occasions, none of these masks will suit your purpose.
Figure 3.1. The property editor for the EditMask property.
I could, for instance, enter the following string as an EditMask:
######;0;_
The purpose of this string is to ensure that the user can enter only numeric values. In particular, these numeric values represent the number of widgets that the user wants to sell.
To understand the EditMask property, you need to study the entry associated with it in the online help. Here, you see all the characters that can be used to define a mask, along with a description of the effects associated with each character. I quote most of this list later in this section.
In the online help for the EditMask property, you will find that a pound sign (#) enables you to enter only numbers, spaces, and plus and minus characters. Notice, however, that there is also a 0 character, which requires that you enter a number in each location marked with that character. The question, then, is which special character should you use: # or 0?
In some cases, the 0 character is not what you want, because it forces the user to enter values. For instance, the following code forces the user to enter a six-digit number, one for each of the zeroes you place in the mask:
000000;0;_
The preceding mask would enable you to enter the number 123456, but it would reject 123. If you want to give the user the freedom to enter a number of any size up to 999,999, you should use the # character rather than the 0 character. One way to sum this matter up is as follows: The 0 character means that the user has to fill in that place with a number, while the # character enables the user to fill in that space with either a number or a blank space.
The zero that appears after the semicolon in the previous string specifies that the mask entered is not saved as part of the data. If I had placed a 1 here, the mask would have been saved. In this particular case, there are no mask characters to either save or discard, so it doesn't matter what I placed in this location. The phone mask, shown previously in Figure 3.1, contains two parentheses that would be affected by this value.
Finally, the very end of the previous mask includes an underscore. This value is used to specify what will be used to designate the space character.
Notice that each field of the mask is separated from the last by a semicolon. Each mask consists of three fields, which were described earlier.
The MaskEdit component is available from the Additional page of the Component palette. Notice that you can click a button in the EditMask dialog found in the EditMask property editor in order to load country-specific strings.
NOTE: The following lists format specifiers you can use in the TMaskEdit control:
! If an ! character appears in the mask, leading blanks don't appear in the data. If an ! character is not present, trailing blanks don't appear in the data.
> If a > character appears in the mask, all characters that follow are in uppercase until the end of the mask or until a < character is encountered.
< If a < character appears in the mask, all characters that follow are in lowercase until the end of the mask or until a > character is encountered.
<> If these two characters appear together in a mask, no case checking is done and the data is formatted with the case the user uses to enter the data.
\ The character that follows a \ character is a literal character. Use this character when you want to allow any of the mask special characters as a literal in the data.
L The L character requires only an alphabetic character only in this position. For the US, this is A-Z, a-z.
l The l character permits only an alphabetic character in this position, but doesn't require it.
A The A character requires an alphanumeric character only in this position. For the US, this is A-Z, a-z, and 0-9.
a The a character permits an alphanumeric character in this position, but doesn't require it.
C The C character requires a character in this position.
c The c character permits a character in this position, but doesn't require it.
0 The 0 character requires a numeric character only in this position.
9 The 9 character permits a numeric character in this position, but doesn't require it.
# The # character permits a numeric character or a plus or minus sign in this position, but doesn't require it.
: The : character is used to separate hours, minutes, and seconds in times. If the character that separates hours, minutes, and seconds is different in the International settings of the Control Panel utility on your computer system, that character is used instead of :.
/ The / character is used to separate months, days, and years in dates. If the character that separates months, days, and years is different in the International settings of the Control Panel utility on your computer system, that character is used instead of /.
; The ; character is used to separate masks.
_ The _ character automatically inserts a blank the edit box. When the user enters characters in the field, the cursor skips the blank character. When using the EditMask property editor, you can change the character used to represent blanks. You can also change this value programmatically. See the following table.
Remember that many of the database objects such as the TDateTime field provide automatic formatting for you.
This is also an area where you might consider turning to third-party products. For instance, Turbo Power Software has an excellent library called Orpheus that provides many fine controls and routines for automatically formatting strings. You should, however, explore the market thoroughly, because there are many fine products out there for handling this type of problem. In particular, many controls are found on the Web. You can visit my Web site to find links to sites that specialize in components. (users.aol.com/charliecal).
The VCL supports a concept called an array of const. This type will enable you to pass in a variable number of parameters. Think for a moment of the sprintf function:
int sprintf(char *buffer, const char *format[, argument, ...]);
The last argument of this function can contain multiple parameters. OpenArrays provide the same functionality in Object Pascal. In short, they enable you to pass a variable number of parameters to a function.
Here is an Object Pascal code segment declared to accept an array of const:
function Format(const Format: string; const Args: array of const): string;
The Format function acts almost exactly like sprintf. Its first parameter contains a string with various format specifiers in it, and its second parameter consists of an array containing multiple values to be formatted. The format specifiers used by Format are essentially the same as those you would use in sprintf. After all, the whole point of adding the function to the Object Pascal language was simply to duplicate the functionality of sprintf inside Object Pascal. Here is the way a call to Format looks in Object Pascal:
var S, Name: string; Age: Integer; begin Name = GetName; Age = GetAge; S := Format("My name is %s and I am %d years old", [Name, Age]); end;
All this is good and well, but you might object that C++ already has support for this kind of syntax through sprintf. That is correct, but you will find many VCL database routines that use arrays of const for various purposes. There are no standard C++ equivalents of these routines, as they exist only in the VCL. In other words, the C++ and Object Pascal implementations of this functionality differ in their details, even though the interface to the user is similar. As a result, special C++ classes need to be created to duplicate this built-in Object Pascal type.
While you may or may not find the Format routine itself useful, you will definitely need to know about arrays of const if you want to work with the VCL, and particularly if you want to do any database programming. For instance, the following database routines all use arrays of const: FinkKey, FindNearest, SetRange, AddRecord, SetKeyFields, AppendRecord, InsertRecord, SetFields, AssignValue, and DBErrorFmt. Clearly, this is a subject that you need to understand.
Here is the declaration for the Format function as it appears in SYSUTILS.HPP:
extern System::AnsiString __fastcall Format(const System::AnsiString Format, const System::TVarRec *Args, const int Args_Size);
As you can see, this function takes not two, but three parameters. You will not, however, pass all three parameters into this function, nor will you pass them into any of the database routines shown previously. Instead, you use a macro called OPENARRAY, which enables you to call VCL functions that use arrays of const with a reasonable degree of ease.
NOTE: As I am sure you have noticed already, this particular subject is not exactly the garden spot of C++Builder. We are definitely in the low-rent district right now, and so perhaps a few more words of explanation are necessary.
Object Pascal originally had no support for functions that took a variable array of parameters. As the spec for Delphi developed, it became clear that there had to be a solution to this problem. The language needed to support functions that took a varying array of parameters because various database calls demanded this functionality. In particular, routines that dealt with keyed fields needed varying numbers of parameters depending on the number of fields present in a composite key.
An array of const was the solution the Object Pascal team came up with for this problem. On the Delphi side, this is a elegant solution that enables you to write simple, easy-to-read code. However, this is not the same solution that the C and C++ languages used when resolving a similar problem. As a result, arrays of const are implemented in an entirely different manner than either the variadic arguments such as you find in sprintf, or the standard C++ practice of function overloading. There was, in short, no common ground between the two languages when it came to this subject.
Hence you find your self wandering down this dark alley, where OpenArrays meet the array of const. I won't make myself look foolish by trying to claim that the solution found here is elegant, but it is usable. Given the nature of the problem the BCB team had thrust upon them, the solution seems reasonable to me.
I also do not feel that this particular low-rent district is typical of BCB neighborhoods. I love BCB components, properties, and events. These are areas where the tool really shines. Open arrays, on the other hand, are at best nothing more than a satisfactory solution to a difficult problem.
One important point to note about the BCB environment, however, is that you can use function overloading in some cases, OPENARRAYS in other cases, or switch to Object Pascal code and use elegant and concise solution provided by an array of const. This environment provides a rich set of tools from which you can pick and choose. Because you have access to the tools available in two different languages, there is no other programming environment on the market that even comes close to the richness of features found in BCB.
Here is a routine that uses both open arrays and sprintf to solve the same problem:
void __fastcall TForm1::Button1Click(TObject *Sender) { char S[150]; if (MoreInput->ShowModal() == mrOk) { sprintf(S, "Hello, my name is %s, and I'm %d years old.", MoreInput->Name, MoreInput->Age); Edit1->Text = S; Edit2->Text = Format("Hello, my name is %s, and I'm %d years old.", OPENARRAY(TVarRec, (MoreInput->Name, MoreInput->Age))); } }
In showing your comparative examples of sprintf and Format, this function makes use of a second form called TMoreInput. I will discuss this form in the next section of the chapter. For now, however, I will forgo discussing multiform projects, and will concentrate instead on using OpenArrays.
As you can see, it doesn't really take any more code to use Format and OPENARRAY than it does to use sprintf. In fact, both techniques have their disadvantages. The sprintf method requires you to declare a temporary variable, the Format method gives you a good dose of the wordy OPENARRAY macro. However, neither technique is particularly difficult to implement.
When using OPENARRAY, you usually pass TVarRec as the first argument of the macro, and then pass a comma-delimited set of variables in the second argument. The entire second argument should be set off by parentheses. Here are a few rather artificial examples of how to use the macro:
OPENARRAY(TVarRec, (1)) OPENARRAY(TVarRec, (1, 2)) OPENARRAY(TVarRec, (1, 2, 3))
And so on, up to, but not beyond, 18. The point is that you pass the variables you want to have processed in the second argument to the macro. You can pass any type in this section, because the macro itself doesn't care. The method that is using the macro may not be so forgiving, however, so you need to put some thought into the code you write. For instance, Format takes only certain types of parameters, and there would be no point in passing something to OPENARRAY that the Format function could not process. The same holds true of sprintf. There would be no point in passing the wrong type of parameter to it, because the format specifiers handle only certain types.
The OPENARRAY macro itself is very simple:
#define OPENARRAY(type, values) \ OpenArray<type>values, OpenArrayCount<type>values.GetHigh()
As you can see, it takes two separate object templates to make the array work. (In fact, TVarRec is a third object, so it takes three objects in all to bring this business home for dinner.) If you are interested, you can open up SysDefs.h and see how this technology is implemented. For the purposes of this book, however, I will simply say that this is one of those cases where you should "pay no attention to the man behind the curtain." I just let the macro do its work and concentrate instead on the parts of the program that are under my jurisdiction.
That is all I'm going to say about OpenArrays. As I conceded earlier, this is not really the garden spot of BCB programming. However, the code I've shown you does get the job done. In particular, the point of this OpenArray business is that it enables you to pass a variant number of parameters to a function. This is a pretty slick trick in itself, and some programmers will probably be able to find occasions when they can use this technology for their own purposes. Its main value, however, is simply that it enables you to use the parts of the VCL that are expecting an array of const.
The sample function shown above called Button1Click has a line in it where a second form is launched:
if (MoreInput->ShowModal() == mrOk)
The program uses this second form to retrieve the name and age input from the user.
Popping up a form to get information from a user is a common task in BCB, so I should perhaps take a moment to explain how it works.
To get started, you should use the File menu, or some other tool, to create a second form for your project. Save it into the same directory where your current project is located. Add two edit controls labeled Name and Age to the second form, and place two TBitBtns on the form, as shown in Figure 3.2. Use the Kind property of the BitBtns to make one an OK button and the second a Cancel button. Notice that the ModalResult property of the OK TBitBtn is automatically set to mrOk, and the ModalResult property of the Cancel TBitBtn is automatically set to mrCancel.
Figure 3.2.The form for the TMoreInput dialog has two labels, two edit controls, and two TBitBtns.
Now create two properties that users of the form can use when they need to access the input from the user:
class TMoreInput : public TForm { // Code omitted here private: AnsiString FName; int FAge; public: virtual __fastcall TMoreInput(TComponent* Owner); __property int Age={read=FAge, write=FAge}; __property AnsiString Name={read=FName, write=FName}; };
As you can see, the Age and Name properties front for two private variables of the object. These variable are assigned to the user's input if the user presses the OK button:
void __fastcall TMoreInput::OkBtnClick(TObject *Sender) { Name = Edit1->Text; Age = Edit2->Text.ToInt();
}
It is now time to go full circle and look back at the stripped-down version of the function that makes use of this dialog:
void __fastcall TForm1::Button1Click(TObject *Sender) { if (MoreInput->ShowModal() == mrOk) { Edit2->Text = Format("Hello, my name is %s, and I'm %d years old.", OPENARRAY(TVarRec, (MoreInput->Name, MoreInput->Age))); }
}
The Button1Click method calls the TForm ShowModal method to launch the dialog. If the user presses the OK button, the input is retrieved from the TMoreInput dialog through the officially designated properties. This enables you to use the private data of TMoreInput without forcing TMoreInput to refrain from ever changing its implementation. For instance, TMoreInput could someday use a struct that contains both the Name and Age fields, but could continue to give users of the form access to data through the Name and Age variables.
If you are interested in this program, you can find it on disk in the Chap02 directory as a file called OpenArrays1. The program itself is simple, but it gives a good illustration of how to use a second dialog to retrieve information from the user. This is a common task that will see many variations run on it in different parts of this book.
Sets are used widely in BCB. Here for instance, is the MsgDialog routine, which is called frequently but requires some knowledge of sets before you can use it correctly:
AnsiString S("While Lust is in his pride, no exclamation \n" "Can curb his heat or rein his rash desire."); MessageDlg(S, mtInformation, TMsgDlgButtons() << mbOK, 0);
The VCL makes heavy use of sets, so there are a number of places in standard BCB database or interface code where you will need to take advantage of the syntax you see here. The goal of the next few pages of this chapter is to explore the BCB Set class and reveal its inner workings.
Sets are easy to understand if you just take a few minutes to absorb their syntax. This is, however, one of those subjects that you have to master if you want to use BCB. In fact, you will find many parts of BCB cumbersome if you don't understand this template class.
Here is a "pseudo template" for the header of the set class designed to help you see how these sets are structured:
Set<Type, MinimumValue, MaximumValue>
More precisely, here is the more formal header declaration from SysDefs.h:
template<class T, unsigned char minEl, unsigned char maxEl>
To declare a set, you write the word Set followed by an open bracket, the type of your set, and its minimum and maximum values. For instance, if you want to create a set of all small letters, you would write
Set<char, `a', `z'> SmallLetters;
To create the set of all numbers between 0 and 9, you might write
Set<int, 0, 9> SmallNumbers;
In the first case, MySet does not consist of the letters a and z, but of a, z, and all the letters between them. In other words, the letter a is the smallest value in the set and the letter z is the maximum value in the set, as shown previously in the formal declaration for the template class.
Both SmallLetters and SmallNumbers are declarations of objects. Often, you don't want to create a new object, but create a new object type with the typedef keyword:
typedef Set<int, 0, 9> TSmallNumbers;
Now you have a type you can work with, and can therefore declare separate instances of this type:
TSmallNumbers SmallNumbers1; TSmallNumbers SmallNumbers2;
The point here is that the first numeric set declared previously, the one called SmallNumbers, is an instance of an object, while TSmallNumbers is the declaration for a type.
NOTE: I'm going to break a rule of this book and spend a few moments on standard C syntax. In particular, I'm going to look at typedefs, because this is an important and easily misunderstood part of the language. The typedef keyword enables you to declare a type within a particular scope. Unfortunately, the #define directive performs a function similar to the typedef keyword, even though the two pieces of syntax have different ways of implementing the result.
For instance, the following two lines have the same effect on your code:typedef float REAL; #define float REALOne asks the compiler to do its work for it, while the second asks the preprocessor to do the work. The result, however, is similar. That is, they give you a new "type" to work with called REAL.
I try to keep some order in my code by always using typedef to declare types, while I generally use #define only to declare constants. No one is perfect, and I probably break that rule from time to time, but I try to follow it with some regularity.
A typedef has effect only within a particular scope. I generally declare types in the header for a unit, unless I want to limit the scope of the type to a particular method or function, in which case I declare the type inside that method or function.
I will not go into this subject any further in this book, nor is the discussion included here meant to be exhaustive. If you want additional information on the typedef keyword, you should turn to a primer on C++. For this book all you need to know is that typedefs provide a way of declaring your own types within a particular scope. I never use it for any other purpose, and I try never to use any other technique for declaring types.
Now that you know the basic facts about sets, it's time to look at some programs that give them a workout. These are important examples, so I will feature them in their own section of the chapter.
The sample program shown in Listings 3.3 and 3.4, called NumberSets, gives you a look at the basic syntax for creating sets. It is followed by a second sample program, called SetBasics, that shows how to create the union, intersection, and difference of two sets.
NOTE: The Set sample programs used in this book originally included some sample components that are found in the Examples directory that ships with BCB. These components were found in Delphi on a page called Samples, but are not installed by default in BCB. If you go into the Examples\Controls directory, you will find an explanation of how to install these components.
In particular, the spin edit control can be a useful tool to use in this kind of program. Check the CD that accompanies this book for an update on this issue and how it affects these programs.
//-------------------------------------------------------------------------- #ifndef MainH #define MainH //-------------------------------------------------------------------------- #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> #include <vcl\Spin.hpp> //-------------------------------------------------------------------------- typedef Set<int, 0, 9> TSmallNum; class TForm1 : public TForm { __published: TButton *AddToSetABtn; TListBox *ListBox1; TSpinEdit *SpinEdit1; TButton *RemoveFromSetBtn; void __fastcall AddToSetABtnClick(TObject *Sender); void __fastcall RemoveFromSetBtnClick(TObject *Sender); private: TSmallNum NumSet; void __fastcall ShowSet(TSmallNum ANumSet, int ListBoxNum); public: virtual __fastcall TForm1(TComponent* Owner); }; //-------------------------------------------------------------------------- extern TForm1 *Form1; //--------------------------------------------------------------------------
#endif
#include <vcl\vcl.h> #pragma hdrstop #include "Main.h" #pragma resource "*.dfm" TForm1 *Form1; __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } void __fastcall TForm1::ShowSet(TSmallNum ASmallNum, int ListBoxNum) { AnsiString S("Set <"); for (int i = 0; i < 275; i++) { if (ASmallNum.Contains(i)) S += "`" + AnsiString(i) + "`,"; } S.SetLength(S.Length() - 1); S += ">"; if (S.Length() == 5) S = "NULL Set"; switch (ListBoxNum) { case 1: ListBox1->Items->Add(S); break; default: ShowMessage("Error specifying listbox"); } } void __fastcall TForm1::AddToSetABtnClick(TObject *Sender) { int i = SpinEdit1->Text.ToInt(); NumSet << i; ShowSet(NumSet, 1); } void __fastcall TForm1::RemoveFromSetBtnClick(TObject *Sender) { int i = SpinEdit1->Text.ToInt(); NumSet >> i; ShowSet(NumSet, 1);
}
NumberSet works with a simple set containing the numbers between 0 and 9:
typedef Set<int, 0, 9> TSmallNum; TSmallNum NumSet;
The main function of the program is to show how to move items in and out of a set. The AddToSetABtnClick method retrieves the number the user places in a TSpinEdit control:
int i = SpinEdit1->Text.ToInt();
It then uses the << operator to insert the new number into the set:
NumSet << i;
A similar method shows how to move information out of the set:
int i = SpinEdit1->Text.ToInt();
NumSet >> i;
The most complicated code in the program involves a method called ShowSet that creates a picture of the set and shows it to the user. The heart of the ShowSet method uses the Contains method of BCB's Set class to see if a particular element is contained in the set:
if (ASmallNum.Contains(i))
Contains returns True if the element is part of the set, and it returns False if the element is not in the set. The results of this effort are shown to the user in a TListBox, as illustrated in Figure 3.3.
Figure 3.3.The NumberSet program creates a visual image of a set so you can get an accurate feeling for how sets work.
The SetBasics program takes the principles used in the NumberSet example and pushes them a little further so you can get a clear sense of what it means to create the intersection, union, or difference of two sets. (See Figure 3.4.) The code for the program is shown in Listing 3.5 and 3.6.
Figure 3.4.The SetBasics program shows how to find the union, difference, or intersection of two sets.
//-------------------------------------------------------------------------- #ifndef MainH #define MainH //-------------------------------------------------------------------------- #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> #include <vcl\Menus.hpp> //-------------------------------------------------------------------------- typedef Set <char, `A', `z'> TLetterSet; class TForm1 : public TForm { __published: TEdit *Edit1; TButton *AddToSetABtn; TListBox *ListBox1; TButton *AddToSetBBtn; TListBox *ListBox2; TListBox *ListBox3; TMainMenu *MainMenu1; TMenuItem *Options1; TMenuItem *Union1; TMenuItem *Intersection1; TMenuItem *Difference1; void __fastcall AddToSetABtnClick(TObject *Sender); void __fastcall AddToSetBBtnClick(TObject *Sender); void __fastcall Union1Click(TObject *Sender); void __fastcall Intersection1Click(TObject *Sender); void __fastcall Difference1Click(TObject *Sender); private: TLetterSet LetterSetA; TLetterSet LetterSetB; void __fastcall ShowSet(TLetterSet ALetterSet, int ListBoxNum); public: virtual __fastcall TForm1(TComponent* Owner); }; //-------------------------------------------------------------------------- extern TForm1 *Form1; //--------------------------------------------------------------------------
#endif
#include <vcl\vcl.h> #pragma hdrstop #include "Main.h" #pragma resource "*.dfm" TForm1 *Form1; __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } void __fastcall TForm1::ShowSet(TLetterSet ALetterSet, int ListBoxNum) { AnsiString S("Set <"); for (int i = 0; i < 275; i++) { if (ALetterSet.Contains(char(i))) S += "`" + AnsiString(char(i)) + "`,"; } S.SetLength(S.Length() - 1); S += ">"; if (S.Length() == 5) S = "NULL Set"; switch (ListBoxNum) { case 1: ListBox1->Items->Add(S); break; case 2: ListBox2->Items->Add(S); break; case 3: ListBox3->Items->Add(S); break; default: ShowMessage("Error specifying listbox"); } } void __fastcall TForm1::AddToSetABtnClick(TObject *Sender) { AnsiString S(Edit1->Text); char ch = S[1]; LetterSetA << ch; ShowSet(LetterSetA, 1); } void __fastcall TForm1::AddToSetBBtnClick(TObject *Sender) { AnsiString S(Edit1->Text); char ch = S[1]; LetterSetB << ch; ShowSet(LetterSetB, 2); } void __fastcall TForm1::Union1Click(TObject *Sender) { TLetterSet Union; Union = LetterSetA + LetterSetB; ShowSet(Union, 3); } void __fastcall TForm1::Intersection1Click(TObject *Sender) { TLetterSet Intersection; Intersection = LetterSetA * LetterSetB; ShowSet(Intersection, 3); } void __fastcall TForm1::Difference1Click(TObject *Sender) { TLetterSet Difference; Difference = LetterSetA - LetterSetB; ShowSet(Difference, 3); }
//--------------------------------------------------------------------------
The SetBasics program uses a set of char instead of a set of integers:
typedef Set <char, `A', `z'> TLetterSet;
The code for moving elements in a set has not changed significantly from the NumberSet example:
void __fastcall TForm1::AddToSetABtnClick(TObject *Sender) { AnsiString S(Edit1->Text); char ch = S[1]; LetterSetA << ch; ShowSet(LetterSetA, 1); } void __fastcall TForm1::AddToSetBBtnClick(TObject *Sender) { AnsiString S(Edit1->Text); char ch = S[1]; LetterSetB << ch; ShowSet(LetterSetB, 2);
}
The difference here, of course, is that two sets are being used, and that you can only add elements to each set.
NOTE: Just before the release of C++Builder, the team changed the way the first character of an AnsiString is accessed. It is now indexed at the first position in the array, rather than at position zero:char ch = S[1]; // First character of string char ch = S[0]; // References nothing, incorrect reference!
This change came so late in the product cycle that I may not reference it correctly in all cases where the subject is brought up in this text. However, I should have caught all references in the code that appears on the CD-ROM that accompanies this book.
Once you have created two sets that interest you, you can press a button on the applications form to see the union, difference, or intersection of the sets. Here, for instance, is how to create the intersection of two sets:
void __fastcall TForm1::Intersection1Click(TObject *Sender) { TLetterSet Intersection; Intersection = LetterSetA * LetterSetB; ShowSet(Intersection, 3); }
The * operator of the Set template is used to perform the operation. You use the + operator to find the union of two sets and the - operator to show the difference between two sets:
Union = LetterSetA + LetterSetB; Difference = LetterSetA - LetterSetB;
The main form for the program is shown in Listing 3.6.
Suppose you have two sets that look like this:
<`A', `B', `E'> <`B', `G'>
The union of the sets would look like this: <`A', `B', `E', `G'>.
The intersection would look like this: <`B'>.
The difference, if you subtracted the second from the first, would look like this: <`A', `E'>.
You can experiment with the program further if you want to see more about creating the intersection, union, or difference of two sets. If you look in SysDefs.h or in the online help, you will see that there are also operators that enable you to test for equality or to see if one set is larger than another.
You can add multiple items to a set with the following syntax:
NumSet << 2 << 3 << 5;
Here, for instance, is a real-world case of adding or removing items from a TNavigator control:
FMyNavigator->VisibleButtons = TButtonSet() << nbFirst << nbLast << nbNext << nbPrior;
In general, sets are very easy to use, though their syntax is not particularly inviting at first glance. Once again, it is important to remember that this Set class is meant for use with the VCL. The Set class in SysDefs.h is not a general purpose set class, and it is not meant as a substitute for the code you find in the Standard Template Library.
As with much of the code shown in this chapter, the key point that justifies the existence of the Set class is its compatibility with certain features of the VCL. Two such features have been illustrated in the last few pages. The first illustrative example is the preceding FMyNavigator code, and the MsgDialog code shown back at the beginning of this discussion of sets:
MessageDlg(S, mtInformation, TMsgDlgButtons() << mbOK, 0);
TMsgDlgButtons is a set class. It has the following declaration:
enum TMsgDlgBtn { mbYes, mbNo, mbOK, mbCancel, mbAbort, mbRetry, mbIgnore, mbAll, mbHelp }; typedef Set<TMsgDlgBtn, mbYes, mbHelp> TMsgDlgButtons;
It should now be obvious how to use this set. The elements of the set consist of all the items in the TMsgDlgBtn enumerated type. The set itself ranges from a low value of mbYes to a high value of mbHelp. If you wanted All and Ignore buttons on a MessageBox, you could write the following code to display it to the user:
MessageDlg(S, mtInformation, TMsgDlgButtons() << mbAll << mbIgnore, 0);
The second parameter to MessageDlg contains a value from the following enumerated type:
enum TMsgDlgType { mtWarning, mtError, mtInformation, mtConfirmation, mtCustom };
You might notice that I frequently refer directly to the various header files that ship with BCB. Getting to know as much as possible about the source for the VCL and for BCB is a worthwhile endeavor. There are times when I use the Tool menu to make it even easier to get at source files that I use a lot.
You can configure the Tools menu in BCB to run any program you like. As a result, I sometimes ask it to start Notepad (also known as Visual Notepad) or some other editor with the source from a key header file in it. To do this
The way this process looks in action is shown in Figure 3.5.
Figure 3.5.Using the Tools menu to create a means for automatically opening a header file whenever you need it.
If possible, and especially if you are using Windows 95, you should find a real editor to use with large header files. Good ones include Visual SlickEdit from MicroEdge, MultiEdit from American Cybernetics, and CodeWright from Premia.
To understand what is happening in your program, you should become familiar with the THeapStatus class from SYSDEFS.HPP:
class THeapStatus { public: Cardinal TotalAddrSpace; Cardinal TotalUncommitted; Cardinal TotalCommitted; Cardinal TotalAllocated; Cardinal TotalFree; Cardinal FreeSmall; Cardinal FreeBig; Cardinal Unused; Cardinal Overhead; Cardinal HeapErrorCode; } ;
To retrieve an instance of this type, you can call GetHeapStatus:
THeapStatus HeapStatus = GetHeapStatus();
The values retrieved by a call to GetHeapStatus tell you about the condition of the heap for your program. They do not refer to global memory for the machine on which your program is running. This call will probably fail if made from inside a DLL.
Variants are an OLE-based type in the VCL that can be assigned to a wide range of variable types. Some people jokingly refer to variants as a typeless type, because it can be used to represent a string, an integer, an object, or several other types.
The following is a simple procedure that illustrates the flexible nature of variants:
void __fastcall TForm1::BitBtn1Click(TObject *Sender) { Variant V; V = 1; V = "Sam";
}
This procedure compiles fine under BCB. As you can see, you are allowed to assign both a string and an integer to the same variant variable.
The following procedure is also legal:
void __fastcall TForm1::BitBtn1Click(TObject *Sender) { Variant V; V = 1; Caption = "Sam" + V;
}
The attempt to assign Edit1.Text to a variant concatenated with a string would have been flagged as a type mismatch by Delphi because a variant in Object Pascal takes on some of the attributes of an integer after being assigned to that type. The Variant class implemented in SYSDEFS.H, however, allows this kind of behavior and handles it properly.
Underneath the surface, variant types are represented by a 16-byte structure found in SYSDEFS.H. At the time of this writing, that structure looks like this:
class TVarData { public: Word VType; Word Reserved1; Word Reserved2; Word Reserved3; union { Smallint VSmallint; Integer VInteger; Single VSingle; Double VDouble; CurrencyBase VCurrency; TDateTimeBase VDate; PWideChar VOleStr; Ole2::IDispatch* VDispatch; Integer VError; WordBool VBoolean; Ole2::IUnknown* VUnknown; Byte VByte; Pointer VString; PVarArray VArray; Pointer VPointer; };
};
Notice that this is a union; that is, the various types represented in the switch statement are overlaid in memory. The structure ends up being 16 bytes in size because the VType field is 2 bytes; the three reserved fields total 6 bytes; and the largest of the types in the variant section is the double, which is 8 bytes in size. (2 + 6 + 8 = 16.) Once again, the switch statement in the declaration is not a list of separate fields, but a list of different ways to interpret the 8 bytes of data contained in the second half of the space allocated for the record.
The following are the declarations for the values used with variants:
#define varEmpty (unsigned char)(0) #define varNull (unsigned char)(1) #define varSmallint (unsigned char)(2) #define varInteger (unsigned char)(3) #define varSingle (unsigned char)(4) #define varDouble (unsigned char)(5) #define varCurrency (unsigned char)(6) #define varDate (unsigned char)(7) #define varOleStr (unsigned char)(8) #define varDispatch (unsigned char)(9) #define varError (unsigned char)(10) #define varBoolean (unsigned char)(11) #define varVariant (unsigned char)(12) #define varUnknown (unsigned char)(13) #define varByte (unsigned char)(17) #define varString (unsigned short)(256) #define varTypeMask (unsigned short)(4095) #define varArray (unsigned short)(8192) #define varByRef (unsigned short)(16384)
NOTE: When you study these declarations, it's important to understand that the VCL defines a certain set of behavior to be associated with variants, but might not guarantee that the implementation will remain the same from version to version. In other words, you can almost surely count on the fact that assigning both a string and an integer to the same variant will always be safe. However, you might not necessarily be sure that variants will always be represented by the same constants and record shown previously. To check whether these implementations have been blessed as being permanent, refer to your VCL documentation.
If I were writing the documentation for the VCL, I might decide not to show you the specific record shown previously because these kinds of details might change in later versions of the product, just as the internal representation for long strings might change. However, in this book I do not claim to be defining the language in a set of official documents. Instead, I am explaining the product to you, using the techniques that seem most useful. In this particular case, I think it helps to see behind the scenes and into the particulars of this implementation of the Variant type. Whether or not this will become a documented fact or an undocumented trick is not clear at the time of this writing.
It should be clear from the preceding code examples that knowing what type is represented by a variant at a particular moment can be important. To check the variant's type, you can use the VarType function, as shown here:
void __fastcall TForm1::BitBtn1Click(TObject *Sender) { int AType; Variant V; AType = VarType(V); // Additional code }
In this example, the Integer variable AType will be set to one of the constants shown previously. In particular, it will probably be assigned to varEmpty, because in the preceding example this variable has not yet been assigned to another type.
If you want to get a closer look at a variant, you can typecast it to learn more about it, as shown in the following example:
void __fastcall TForm1::BitBtn1Click(TObject *Sender) { Variant V; TVarData VarData; int AType; V = 1; VarData = TVarData(V); AType = VarData.VType; }
In this particular case, AType will be set to the same value that would be returned from a call to VarType.
To understand more about how all this works, see Listing 3.7 and 3.8, in which you can find the code for the VarDatas program. This code sets a single variant to a series of different values and then examines the variant to discover the current value of its VType field.
/////////////////////////////////////// // Copyright (c) 1997 by Charlie Calvert // #ifndef MainH #define MainH #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> class TForm1 : public TForm { __published: TButton *bVariantPlay; TListBox *ListBox1; void __fastcall bVariantPlayClick(TObject *Sender); private: void ShowVariant(Variant &V); public: virtual __fastcall TForm1(TComponent* Owner); }; extern TForm1 *Form1;
#endif
/////////////////////////////////////// // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "Main.h" #pragma resource "*.dfm" TForm1 *Form1; __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } AnsiString GetVariantType(Variant &V) { TVarData VarData; AnsiString S; VarData = TVarData(V); switch (VarData.VType) { case varEmpty: S = "varEmpty"; break; case varNull: S = "varNull"; break; case varSmallint: S = "varSmallInt"; break; case varInteger: S = "varInteger"; break; case varSingle: S = "varSingle"; break; case varDouble: S= "varDouble"; break; case varCurrency: S = "varCurrency"; break; case varDate: S = "varDate"; break; case varOleStr: S = "varOleStr"; break; case varDispatch: S = "varDispatch"; break; case varError: S = "varError"; break; case varBoolean: S = "varBoolean"; break; case varVariant: S = "varVariant"; break; case varUnknown: S = "varUnknown"; break; case varString: S = "varString"; break; case varTypeMask: S = "varTypeMask"; break; case varByRef: S = "varByRef"; break; case varByte: S = "varByte"; break; case varArray: S = "varArray"; break; default: S = "Error"; } return S; } void TForm1::ShowVariant(Variant &V) { AnsiString S, Temp; if ((VarIsEmpty(V) == True) || (VarIsNull(V) == True)) { Temp = "Null"; S = Format("Value: %-15s Type: %s", OPENARRAY(TVarRec, (Temp, GetVariantType(V)))); } else { Temp = V; S = Format("Value: %-15s Type: %s", OPENARRAY(TVarRec , (Temp, GetVariantType(V)))); } ListBox1->Items->Add(S); } void __fastcall TForm1::bVariantPlayClick(TObject *Sender) { Variant V; ShowVariant(V); V = Null; ShowVariant(V); V = 1; ShowVariant(V); V = "Sam"; ShowVariant(V); V = 1.25; ShowVariant(V);
}
A typical run of this program is shown in Figure 3.6.
Figure 3.6.The VarDatas program uses the TVarData structure to examine how variants are put together.
The data shown in the list box in Figure 3.6 represents the value and internal representation of a single variant that is assigned a series of different types. It's important to understand that Variant can't simultaneously represent all types, and can instead at any one moment take on the characteristics of only one particular type.
The code that assigns different types to a variant is easy to understand, as is shown here:
void __fastcall TForm1::bVariantPlayClick(TObject *Sender) { Variant V; ShowVariant(V); V = Null; ShowVariant(V); V = 1; ShowVariant(V); V = "Sam"; ShowVariant(V); V = 1.25; ShowVariant(V);
}
The code that reports on the current type of a variant is somewhat more complex, but still relatively straightforward, as shown next:
AnsiString GetVariantType(Variant &V) { TVarData VarData; AnsiString S; VarData = TVarData(V); switch (VarData.VType) { case varEmpty: S = "varEmpty"; break; case varNull: S = "varNull"; break; case varSmallint: S = "varSmallInt"; break; case varInteger: S = "varInteger"; break; case varSingle: S = "varSingle"; break; case varDouble: S= "varDouble"; break; case varCurrency: S = "varCurrency"; break; case varDate: S = "varDate"; break; case varOleStr: S = "varOleStr"; break; case varDispatch: S = "varDispatch"; break; case varError: S = "varError"; break; case varBoolean: S = "varBoolean"; break; case varVariant: S = "varVariant"; break; case varUnknown: S = "varUnknown"; break; case varString: S = "varString"; break; case varTypeMask: S = "varTypeMask"; break; case varByRef: S = "varByRef"; break; case varByte: S = "varByte"; break; case varArray: S = "varArray"; break; default: S = "Error"; } return S; }
This code first converts a variant into a variable of type TVarData. In doing so, it is merely surfacing the true underlying type of the variant. However, a variable of type TVarData will not act the same as a variable of type Variant. This is because the compiler provides special services for variants that it would not provide for a simple record type such as TVarData.
It's important to note that there are at least two ways to write the first lines of code in this function. For instance, I could have written
AnsiString GetVariantType(Variant &V) { AnsiString S; switch (VarType(V)) { case varEmpty: S = "varEmpty"; break; case varNull: S = "varNull"; break; // Code omitted } }
This code works the same as the code shown in the actual program found on the CD-ROM.
However you decide to implement the function, the key point is that variants can take on the appearance of being of a certain type. The chameleon-like behavior of Variant is sparked by the type of variable to which it is assigned. If you want, you can think of a variant as a chameleon that hides itself from view by assuming the coloration of the variable to which it is assigned. A variant is never of type Variant; it's always either empty, NULL, or the type of the variable to which it is assigned. In the same way, a chameleon has no color of its own but is always changing to adapt its color to the environment around it. Either that, or it is unborn, dead, nonexistent, or has no color at all!
The following routines can all be used with variants. To learn more about these routines, look in the online help.
extern void __fastcall VarClear(Variant &V); extern void __fastcall VarCopy(Variant &Dest, const Variant &Source); extern void __fastcall VarCopyNoInd(Variant &Dest, const Variant &Source); extern void __fastcall VarCast(Variant &Dest, const Variant &Source, int VarType); extern int __fastcall VarType(const Variant &V); extern Variant __fastcall VarAsType(const Variant &V, int VarType); extern bool __fastcall VarIsEmpty(const Variant &V); extern bool __fastcall VarIsNull(const Variant &V); extern AnsiString __fastcall VarToStr(const Variant &V); extern Variant __fastcall VarFromDateTime(TDateTime DateTime); extern TDateTime __fastcall VarToDateTime(const Variant &V);
Here are some routines to use with Variant arrays.
extern Variant __fastcall VarArrayCreate(const int *Bounds, const int Bounds_Size,int VarType); extern Variant __fastcall VarArrayOf(const Variant *Values, const int Values_Size); extern void __fastcall VarArrayRedim(Variant &A, int HighBound); extern int __fastcall VarArrayDimCount(const Variant &A); extern int __fastcall VarArrayLowBound(const Variant &A, int Dim); extern int __fastcall VarArrayHighBound(const Variant &A, int Dim); extern void * __fastcall VarArrayLock(const Variant &A); extern void __fastcall VarArrayUnlock(const Variant &A); extern Variant __fastcall VarArrayRef(const Variant &A); extern bool __fastcall VarIsArray(const Variant &A);
Before closing this section, I want to make it clear that variants are not meant to be used broadly in your program whenever you need to work with a variable. I have no doubt that some people will in fact program that way, but I want to emphasize that the developers didn't really want variants to be used that way. This is mainly because they have some overhead associated with them that can slow down the compiler. Some tricks performed by variants, such as string manipulation, happen to be highly optimized. However, you should never consider a variant to be as fast or efficient as a standard C type.
Variants are almost certainly best used in OLE and database applications. In particular, variants were brought into the language because they play a role in OLE automation. Furthermore, much of the structure and behavior of variants is defined by the rules of OLE. These unusual types also prove to be useful in database applications. As a rule, there is so much overhead involved in referencing any value from a database that a little additional variant manipulation code is not going to make a significant difference.
Some of the best features of C++Builder are not immediately obvious to you when you first start working with the tool, or even after you have been working with the tool for awhile. For instance, C++Builder has a built in type called a TStringList that all but makes obsolete the concept of a text file. This class is so easy to use, and so powerful, that there are few occasions when you would want to open a text file using fopen. It's important to note that manipulating text files is not really the central purpose of the TStringList class; however, it performs this task so well that there is little need to ever resort to using fopen, fclose, and other similar commands.
Before looking at how you would use to TStringList to open a text file, it might be helpful to point out the places in BCB where this class is most often used, as well as a few other features of the class.
The TStringList class is a descendant of the abstract class TStrings. There are no instances of the TString class in BCB, because this is an abstract type. However, the TStringList, TListBox->Items, TComboBox->Items, TMemo->Lines, TRichEdit->Lines and TQuery->VCL classes all descend directly from TStrings. This means you can assign instances of these classes to one another.
You can, for instance, create an instance of the TStringList class and assign it to a TListBox->Items object:
TStringList *MyStringList = new TStringList; MyStringList->Add("Some text"); MyStringList->Add("More text"); ListBox1->Items = MyStringList;
If you write code like this, ListBox1 will end up showing two items in it, with the first being "Some text", and the second being "More text".
You can write the contents of any string list to file by using the SaveToFile method, and you can read the list back using LoadFromFile. There are also SaveToStream and LoadFromStream methods:
MyStringList->SaveToFile("C:\\Sam.txt"); MyStringList->LoadFromFile("C:\\Sam.txt");
TStringLists have a sort property that can enable you to sort them instantly. You can also associate an object with each string in the TStringList using the InsertObject method.
Given the presence of all this flexibility in one simple-to-use object, it does not really make sense to use any other tool for handling lists of strings. It is definitely worth taking time to explore this object and to see what it can do for you.
I will spend little time in this book talking about how to pass switches to the compiler. That subject simply is of no interest to programmers whose primary goal is rapid application development. However, even under ideal conditions, there may be times when a programmer may have to take off their RAD hat and put on their Systems programming hat. So, if you absolutely need to start tweaking the compiler, here are some new BCC32.EXE switches culled from the online help:
New BCC32.EXE switches:
New BCC32.EXE pragmas:
New switches for the Pascal DCC32.EXE compiler:
One of the biggest problems with BCB forms is getting them to look correct in all the possible resolutions. There is probably no surefire method for achieving these ends. However, there are some tips that I have found that help.
When I build forms I almost always set the AutoScroll property to False. If you do not do this, your form will always stay the same size, which means that the edges of your form will be obscured in some resolutions. To avoid this, you would have to resize the form to fit around the edges of your controls when you changed resolutions.
Whatever approach you take, you simply have to view your form in at least three or four different modes before releasing it to the public. In particular, be sure to switch back and forth between Large Fonts and Small Fonts when you change resolutions. If you are a complete perfectionist, you may decide that the only solution is to have two sets of forms, one for use with Small Fonts, and the other for use with Large Fonts.
Some people have a definite knack for designing good-looking forms. If you find someone who can do this, ask them to critique your forms or even to help you design them. If you find forms that you like, study them. Use whatever tricks you can find to help you create decent-looking forms. Try to leave some whitespace on your forms if you can, and make sure controls line up vertically and horizontally whenever possible.
In this chapter you saw an overview of all the key areas in which BCB and the VCL meet. This is a complicated, yet extremely important aspect of the product.
I'm sure it tried your patience at times to have to go through so much dry material. However, if you have this subject firmly under your belt, you are ready to begin a serious study of BCB. You now know a good deal about how this product is put together, and why it was constructed in this particular fashion. This is the kind of knowledge for which there is no substitute. You simply have to know these kinds of things about a product before you can take advantage of its best features.
BCB is a tool for using components and objects to make applications. Any type of application you can conceive of creating can be made better in Borland C++Builder than it can with any other C/C++ compiler. What do I mean by better? I mean that it can be put together faster, with a better chance of having the right design, and with fewer bugs. In order to get that power, BCB had to tap into the VCL. In this chapter you saw how it was done.
One of the most important themes of this chapter was the central role played by components, properties, and events in the BCB programming model. This product does a lot of great things, but the key features that make it all possible are the support for components, properties, and events. Much of the groundwork for that support was laid out in this chapter.
©Copyright, Macmillan Computer Publishing. All rights reserved.